메인 콘텐츠로 이동하기
  1. 서버구축기/

MariaDB기반 MultiDomain

·10 분·

MariaDB기반 Multi Domain #

파일시스템기반 multi domain설정을 mariaDB기반 multi domain설정으로 변경하는 과정을 기록 했습니다. 누락된 정보가 있을 수 있습니다. ai에게 도움을 받으면 부족한 부분을 충분히 채울 수 있으리라 생각 합니다. 여기까지 오는데 ai의 도움을 많이 받았습니다. 초보 분들이 메일서버 설정에서 ai와 무한 루프에 빠지는 일이 없기를 바라는 마음으로 기록 남깁니다. 도움이 되기를 바랍니다.

대략적인 구조입니다 #

Postfix Mysql 구조

Postfix
   ├─ mysql-virtual-domains.cf  ──► virtual_domains
   ├─ mysql-virtual-users.cf    ──► virtual_users
   ├─ mysql-virtual-aliases.cf  ──► virtual_aliases
   ├─ mysql-sender-canonical.cf ──► sender_canonical
   └─ mysql-virtual-sender-maps.cf ──► virtual_users

                      MariaDB
                    /    |    \
           virtual_domains
           virtual_users
           virtual_aliases
           sender_canonical
                Postfix
                  │ LMTP
                Dovecot
               Maildir

.cf 파일은 Postfix가 “이 조건일 때 MariaDB에 어떤 쿼리를 날려서 답을 받아올지"를 정의하는 연결 설정입니다. Postfix 자체는 DB를 직접 모르고, 이 파일들을 통해서만 MariaDB와 대화합니다.

새로 생성하거나 수정할 파일목록입니다 #

sudo nano /etc/dovecot/conf.d/10-ssl.conf
sudo nano /etc/dovecot/dovecot-sql.conf.ext
sudo nano /etc/postfix/mysql-sender-canonical.cf
sudo nano /etc/postfix/mysql-virtual-domains.cf
sudo nano /etc/postfix/mysql-virtual-users.cf
sudo nano /etc/postfix/mysql-virtual-aliases.cf
sudo nano /etc/postfix/mysql-virtual-sender-maps.cf
sudo nano /etc/postfix/master.cf
sudo nano /etc/postfix/main.cf
sudo nano /etc/dovecot/dovecot.conf
sudo nano /etc/dovecot/conf.d/10-mail.conf
sudo nano /etc/dovecot/conf.d/10-master.conf
sudo nano /etc/dovecot/conf.d/20-lmtp.conf
sudo nano /etc/dovecot/conf.d/10-auth.conf
sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf

기본 시스템 및 MariaDB 준비 #

MariaDB 설치

sudo apt update
sudo apt install mariadb-server mariadb-client -y

데이터베이스의 자물쇠 설치 (초기 보안 설정 마법사)

sudo mysql_secure_installation
  • 처음 설치 시 첫 항목 그냥 enter
  • Switch to unix_socket authentication? → n
  • Change root password? → y (password 입력)
  • Remove anonymous users? → y
  • Disallow root login remotely? → y
  • Remove test database? → y
  • Reload privilege tables now? → y

Postfix에게 주소록 연결을 위한 설치

sudo apt install postfix-mysql -y

Dovecot에게 사용자 정보 연결을 위한 설치

sudo apt install dovecot-mysql -y

메일을 전송하는 효율적인 통로 설치 (LMTP)

sudo apt install dovecot-lmtpd -y

MariaDB 시작

sudo systemctl daemon-reload
sudo systemctl enable mariadb
sudo systemctl start mariadb
sudo systemctl status mariadb

메일 전용 DB 및 계정 생성 #

Postfix/Dovecot은 조회(SELECT)만 하면 되므로 mailuser는 읽기 권한만, 관리 스크립트나 웹 UI에서 쓰는 mailadmin은 쓰기 권한까지 분리해서 부여합니다. 이렇게 권한을 나눠두면 메일 처리 경로 쪽 코드에 문제가 생겨도 DB 내용이 함부로 바뀌지 않습니다.

CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'mailuser'@'127.0.0.1' IDENTIFIED BY 'password';
GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1';

CREATE USER 'mailuser'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON mailserver.* TO 'mailuser'@'localhost';

CREATE USER 'mailadmin'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mailserver.* TO 'mailadmin'@'localhost';

FLUSH PRIVILEGES;

DB 접속 후 확인

mariadb -u root -p
USE mailserver;

-- 현재 어떤 계정으로 접속했는지 확인
SELECT USER(), CURRENT_USER();

-- mailserver DB가 실제로 생성됐는지 확인
SHOW DATABASES LIKE 'mailserver';

-- 전체 DB 계정 목록
SELECT Host, User FROM mysql.user;

-- mailadmin 계정만 확인
SELECT Host, User FROM mysql.user WHERE User = 'mailadmin';

-- mailadmin에게 부여된 권한 확인
SHOW GRANTS FOR 'mailadmin'@'localhost';

-- 인증 방식(plugin) 확인
SELECT Host, User, plugin, authentication_string FROM mysql.user;

테이블 구조 설계 #

CREATE TABLE virtual_domains (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL UNIQUE,
  active TINYINT(1) NOT NULL DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE virtual_users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  domain_id INT NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  password VARCHAR(255) NOT NULL,
  quota BIGINT NOT NULL DEFAULT 0,
  active TINYINT(1) NOT NULL DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  KEY idx_domain_active (domain_id, active),
  KEY idx_email_active (email, active),
  CONSTRAINT fk_users_domain
    FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
    ON DELETE CASCADE
) ENGINE=InnoDB;

CREATE TABLE virtual_aliases (
  id INT AUTO_INCREMENT PRIMARY KEY,
  domain_id INT NOT NULL,
  source VARCHAR(255) NOT NULL,
  destination VARCHAR(255) NOT NULL,
  active TINYINT(1) NOT NULL DEFAULT 1,
  KEY idx_source_active (source, active),
  KEY idx_domain (domain_id),
  KEY idx_destination (destination),
  CONSTRAINT fk_alias_domain
    FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
    ON DELETE CASCADE
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS sender_canonical (
    id INT AUTO_INCREMENT PRIMARY KEY,
    source VARCHAR(255) NOT NULL,
    destination VARCHAR(255) NOT NULL,
    active TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY unique_source (source),
    KEY idx_active (active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

active 컬럼으로 계정/도메인 활성화 관리 가능. (삭제 대신 active=0으로 바꾸면 이력이 남아서 복구가 쉽습니다.)

기존 파일 데이터 DB 마이그레이션 #

도메인 입력

INSERT INTO virtual_domains(name, active) VALUES
  ('domain1.com', 1),
  ('domain2.com', 1),
  ('domain3.com', 1),
  ('domain4.com', 1),
  ('domain5.com', 1),
  ('domain6.com', 1),
  ('domain7.com', 1),
  ('domain8.com', 1);

도메인 전체 목록 확인

SELECT * FROM virtual_domains;

특정 도메인이 활성화 상태인지 확인

SELECT name FROM virtual_domains WHERE name='domain1.com' AND active=1;

사용자 입력 (Dovecot의 SHA512-CRYPT 해시를 그대로 사용)

INSERT INTO virtual_users (domain_id, email, password) VALUES
  (1, 'user1@domain1.com', '{SHA512-CRYPT}hash1'),
  (1, 'user2@domain1.com', '{SHA512-CRYPT}hash2'),
  (2, 'user1@domain2.com', '{SHA512-CRYPT}hash3'),
  (2, 'user2@domain2.com', '{SHA512-CRYPT}hash4'),
  (3, 'user1@domain3.com', '{SHA512-CRYPT}hash5'),
  (3, 'user2@domain3.com', '{SHA512-CRYPT}hash6'),
  (4, 'user1@domain4.com', '{SHA512-CRYPT}hash7'),
  (4, 'user2@domain4.com', '{SHA512-CRYPT}hash8')
  /* ... */;

시스템 계정인 root가 메일을 발송하면, 외부에는 master@domain1.com이 보낸 것으로 표시됩니다.

INSERT INTO sender_canonical (source, destination, active)
VALUES
  ('root', 'master@domain1.com', 1),
  ('root@domain1.com', 'master@domain1.com', 1);

시스템 내부에서 자동으로 생성되는 메일들의 “보낸 사람” 주소가 master@domain1.com로 바뀝니다.

INSERT INTO sender_canonical (source, destination, active)
VALUES
  ('www-data', 'master@domain1.com', 1),
  ('postfix', 'master@domain1.com', 1),
  ('vmail', 'master@domain1.com', 1),
  ('cron', 'master@domain1.com', 1);

INSERT INTO virtual_aliases (domain_id, source, destination) VALUES
  (1, 'info@domain1.com', 'master@domain1.com'),
  (1, 'admin@domain1.com', 'master@domain1.com'),
  (1, 'webmaster@domain1.com', 'master@domain1.com'),
  (1, 'postmaster@domain1.com', 'dmarc@domain1.com'),
  (2, 'info@domain2.com', 'master@domain2.com'),
  (3, 'info@domain3.com', 'master@domain3.com'),
  (4, 'info@domain4.com', 'master@domain4.com'),
  (5, 'info@domain5.com', 'master@domain5.com'),
  (6, 'info@domain6.com', 'master@domain6.com'),
  (7, 'info@domain7.com', 'master@domain7.com'),
  (8, 'info@domain8.com', 'master@domain8.com');

전체를 리스트로 보고 싶을 때 사용합니다.

SELECT * FROM virtual_aliases;

\G 옵션을 사용하면 각 행을 세로로 깔끔하게 나열해 줍니다.

SELECT * FROM virtual_aliases\G
SELECT DISTINCT source FROM virtual_aliases ORDER BY source;

특정 도메인의 별칭만 확인하기

SELECT * FROM virtual_aliases
WHERE source LIKE '%@domain1.com';

SELECT * FROM sender_canonical;
SELECT * FROM sender_canonical WHERE source = 'vmail';

단독 조회 테스트 (postmap으로 실제 Postfix가 보는 것과 동일한 방식으로 쿼리 결과 확인)

sudo postmap -q root mysql:/etc/postfix/mysql-sender-canonical.cf
sudo postmap -q root@domain1.com mysql:/etc/postfix/mysql-sender-canonical.cf
sudo postmap -q nobody mysql:/etc/postfix/mysql-sender-canonical.cf

Postfix + MariaDB 연동 #

MariaDB용 Postfix CF 파일 생성

sudo nano /etc/postfix/mysql-virtual-domains.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-users.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-aliases.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s' AND active=1
sudo nano /etc/postfix/mysql-virtual-sender-maps.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s' AND active=1
sudo nano /etc/postfix/mysql-sender-canonical.cf
user = mailuser
password = password
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM sender_canonical WHERE source='%s' AND active=1

Dovecot + MariaDB 연동 #

sudo nano /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailuser password=password
default_pass_scheme = SHA512-CRYPT
password_query = \
  SELECT email as user, password \
  FROM virtual_users \
  WHERE email='%u' AND active=1
user_query = \
  SELECT '/var/mail/vhosts/%d/%n' as home, 1005 as uid, 1005 as gid \
  FROM virtual_users \
  WHERE email='%u' AND active=1

권한 설정 #

Postfix/Dovecot이 읽어야 하는 설정 파일에는 비밀번호가 평문으로 들어있으므로, 해당 서비스 그룹만 읽을 수 있게 권한을 좁혀둡니다.

sudo chown root:postfix /etc/postfix/mysql-virtual-*.cf
sudo chmod 640 /etc/postfix/mysql-virtual-*.cf
sudo chown root:postfix /etc/postfix/mysql-sender-canonical.cf
sudo chmod 640 /etc/postfix/mysql-sender-canonical.cf
sudo chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
sudo chmod 640 /etc/dovecot/dovecot-sql.conf.ext

Postfix에 방금 만든 연동 파일들을 등록합니다.

sudo postconf -e "sender_canonical_maps = mysql:/etc/postfix/mysql-sender-canonical.cf"
sudo postconf -e "virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf"
sudo postconf -e "virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-users.cf"
sudo postconf -e "virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf"
sudo postconf -e "smtpd_sender_login_maps = mysql:/etc/postfix/mysql-virtual-sender-maps.cf"
sudo postconf -e "sender_canonical_classes = envelope_sender,header_sender"

Dovecot을 통해 인증하도록 설정 추가 후 테스트 #

sudo postconf -e "smtpd_sasl_type = dovecot"
sudo postconf -e "smtpd_sasl_path = private/auth"
sudo postconf -e "smtpd_sasl_auth_enable = yes"
sudo postconf -e "smtpd_sasl_security_options = noanonymous"

Test

sudo postmap -q domain1.com mysql:/etc/postfix/mysql-virtual-domains.cf
sudo postmap -q master@domain1.com mysql:/etc/postfix/mysql-virtual-users.cf
sudo postmap -q master@domain1.com mysql:/etc/postfix/mysql-virtual-sender-maps.cf
  • virtual_domains1
  • virtual_users1
  • virtual_sender_mapsemail

결과값이 11master@domain1.com 순서로 출력되면 성공입니다.

10-auth.conf 수정 #

sudo nano /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = no
auth_mechanisms = plain login
!include auth-sql.conf.ext

auth-client 소켓 설정 #

sudo nano /etc/dovecot/conf.d/10-master.conf
service anvil {
  client_limit = 2000
}
service auth {
  client_limit = 2000
  process_limit = 1
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }

  unix_listener auth-userdb {
    mode = 0660
    user = vmail
    group = vmail
  }
}

# Dovecot Tuning
service imap-login {
  process_limit = 500
  client_limit = 1000
}

# LMTP Socket Setting
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0660
    user = postfix
    group = postfix
  }
}

/etc/dovecot/dovecot.conf 설정 #

sudo nano /etc/dovecot/dovecot.conf
mail_max_userip_connections = 50
protocols = imap lmtp
auth_cache_size = 50M
auth_cache_ttl = 2 hours
ssl = required

/etc/postfix/master.cf 설정 #

sudo nano /etc/postfix/master.cf
smtp        inet  n       -       n       -       -       smtpd
submission  inet  n       -       n       -       -       smtpd
submissions inet  n       -       n       -       -       smtpd
smtp        unix  -       -       n       -       -       smtp
proxymap    unix  -       -       n       -       -       proxymap
cleanup     unix  n       -       n       -       0       cleanup
rewrite     unix  -       -       n       -       -       trivial-rewrite
local       unix  -       n       n       -       -       local
lmtp        unix  -       -       n       -       -       lmtp

/etc/dovecot/conf.d/10-mail.conf 설정 #

sudo nano /etc/dovecot/conf.d/10-mail.conf
# mail_location = maildir:/var/mail/vhosts/%d/%n
# INBOX NAMESPACE
namespace inbox {
  type = private
  separator = /
  prefix =
  location = maildir:/var/mail/vhosts/%d/%n
  inbox = yes

  mailbox Drafts {
    special_use = \Drafts
    auto = subscribe
  }

  mailbox Junk {
    special_use = \Junk
    auto = subscribe
  }

  mailbox Sent {
    special_use = \Sent
    auto = subscribe
  }

  mailbox Trash {
    special_use = \Trash
    auto = subscribe
  }
}

maildir_stat_dirs = yes
maildir_copy_with_hardlinks = yes
mail_privileged_group = mail
mail_access_groups = mail
lock_method = fcntl

/etc/dovecot/conf.d/20-lmtp.conf 설정 #

sudo nano /etc/dovecot/conf.d/20-lmtp.conf
protocol lmtp {
  postmaster_address = master@domain1.com
  mail_plugins = $mail_plugins
}

/etc/mysql/mariadb.conf.d/50-server.cnf 설정 #

MariaDB 성능 튜닝

sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld]
bind-address = 127.0.0.1

# innodb_buffer_pool_size는 서버 RAM 8GB 기준 2G로 설정 (RAM 용량이 다르면 그에 맞게 조정)
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2

# MariaDB connection
max_connections = 500
thread_cache_size = 100
table_open_cache = 2000

# slow query (monitoring)
slow_query_log     = 1
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time    = 2

/etc/postfix/main.cf 설정 #

Postfix 동시 처리량

sudo nano /etc/postfix/main.cf
default_process_limit = 200
smtp_destination_concurrency_limit = 20
smtp_destination_rate_delay = 1s
minimal_backoff_time = 300s
maximal_backoff_time = 4000s
queue_run_delay = 300s
smtpd_client_connection_count_limit = 20
smtpd_client_connection_rate_limit = 30
virtual_transport = lmtp:unix:private/dovecot-lmtp

마지막 점검 #

모든 디렉토리를 700으로 (vmail 유저만 접근 가능)

sudo chown -R vmail:vmail /var/mail/vhosts
sudo find /var/mail/vhosts -type d -exec chmod 700 {} +

모든 파일을 600으로 (불필요한 실행 권한 제거)

sudo find /var/mail/vhosts -type f -exec chmod 600 {} +

소켓 파일이 존재하는지 확인 (srw------- 타입)

sudo ls -l /var/spool/postfix/private/dovecot-lmtp

Dovecot이 관리하는 프로토콜 목록 확인

sudo doveconf protocols

결과에 imap lmtp가 포함되어야 함

서비스 재시작 및 점검 #

sudo postfix check
sudo systemctl restart mariadb
sudo systemctl restart dovecot
sudo systemctl restart postfix

DB에러시 확인 #

sudo journalctl -xeu mariadb.service | tail -n 20

# MariaDB 설정 파일 문법 검증 (버전에 따라 이 옵션이 없을 수 있으니,
# 실패하면 아래처럼 dry-run으로 기동 여부만 확인하는 방법도 있습니다)
sudo mariadbd --validate-config
# 대안: sudo mariadbd --help --verbose 로 지원 옵션 목록 확인

sudo doveadm auth test user1@domain1.com
ls -l /var/run/dovecot/auth-userdb
sudo ls -l /var/spool/postfix/private/auth
sudo ls -l /var/spool/postfix/private/dovecot-lmtp
sudo ls -l /var/run/dovecot/auth-client

파일시스템 점검 #

sudo nano /etc/fstab
/dev/disk/by-id/dm-uuid-***  /      ext4  defaults,noatime,nodiratime  0  1
/dev/disk/by-uuid/***        /boot  ext4  defaults,noatime             0  2
/swap.img                    none   swap  sw                           0  0

절대 바로 재부팅하지 마세요

fstab을 잘못 고치면 부팅 자체가 안 될 수 있습니다. 아래 명령어로 마운트 테스트를 먼저 수행하세요.
sudo mount -a

아무런 메시지도 나오지 않아야 정상입니다. 만약 에러가 난다면 즉시 /etc/fstab을 다시 열어 오타를 수정하세요.

mount | grep -E '/ |/boot'
/dev/mapper/ubuntu--vg-ubuntu--lv on / type ext4 (rw,noatime,nodiratime)
/dev/sda2 on /boot type ext4 (rw,noatime)

적용이 안 됐다면

sudo mount -o remount,noatime /boot

마지막 확인 #

설정 파일 내에서 ssl_cert와 ssl_key 항목 확인

grep -E "ssl_cert|ssl_key" /etc/dovecot/conf.d/10-ssl.conf

Postfix hostname (중요) — 이거 안 맞으면 메일 헤더, TLS, spam score 전부 꼬일 수 있음

sudo nano /etc/hostname
mail.domain1.com
sudo nano /etc/hosts
127.0.0.1      localhost
203.0.113.10   mail.domain1.com mail
sudo nano /etc/postfix/main.cf
myhostname = mail.domain1.com
mydomain = domain1.com
myorigin = $mydomain

결과

hostname
# mail.domain1.com

hostname -f
# mail.domain1.com

postconf myhostname
# mail.domain1.com

인증서 외 #

sudo nano /etc/postfix/main.cf
smtpd_tls_security_level = may
smtp_tls_security_level = may
mydestination = localhost

# Postfix 기본값이지만 명시하는 게 안전 — 없으면 queue 권한 문제가 가끔 발생함
virtual_transport = lmtp:unix:private/dovecot-lmtp
mail_owner = postfix

# 메일 호스팅에서는 기본값(약 50MB)이 문제됨 — 0은 무제한
mailbox_size_limit = 0

# 대용량 첨부 대비 (50MB)
message_size_limit = 52428800

Dovecot auth socket 권한 확인 필수

sudo ls -l /var/spool/postfix/private/auth

정상 예시: srw-rw---- postfix postfix 틀리면 SASL authentication failure 발생함